Skip to content

Fix: document the gcloud pass-through of adk deploy cloud_run in --help - #587

Open
AmaadMartin wants to merge 3 commits into
fix/cloud-run-passthrough-gcloud-argsfrom
fix/cloud-run-deploy-help-text
Open

Fix: document the gcloud pass-through of adk deploy cloud_run in --help#587
AmaadMartin wants to merge 3 commits into
fix/cloud-run-passthrough-gcloud-argsfrom
fix/cloud-run-deploy-help-text

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    N/A — no existing public issue.
  2. Or, if no issue exists, describe the change:

Stacked on #447 (fix/cloud-run-passthrough-gcloud-args). See
Collision check at the bottom — that PR rewrites the same pass-through
collector, so this branch is built on top of it and targets its branch, not
main.

Problem: adk deploy --help documents nothing about deploying.

  • All three deploy subcommands are registered without .description()
    (dev/src/cli/cli.ts: cloud_run at the DEPLOY_COMMAND.command('cloud_run')
    call, and agent_engine / reasoning_engine through
    registerAgentEngineCommand), unlike web, api_server, create and run.
    Commander therefore prints three blank rows:

    Commands:
      cloud_run [options] [agents_dir]
      agent_engine [options] [agents_dir]
      reasoning_engine [options] [agents_dir]
      help [command]                        display help for command
    
  • adk deploy cloud_run forwards every option it does not recognize verbatim to
    gcloud run deploy, and rejects a handful of them because ADK sets them
    itself (validateGcloudExtraArgs in
    dev/src/cli/deploy/cli_deploy_cloud_run.ts). Neither half of that contract is
    written down anywhere in the repo — grep -r cloud_run docs/ README.md CONTRIBUTING.md returns nothing, so the help output is the documentation
    surface, and it was silent. The only ways to discover the pass-through were to
    read the source or to hit the error.

Solution: help text only — no parsing behaviour changes.

  • .description('Deploys an agent to Cloud Run') on cloud_run, and
    .description('Deploys an agent to Vertex AI Agent Engine') inside
    registerAgentEngineCommand (which serves both agent_engine and
    reasoning_engine), so no row in adk deploy --help is blank.

  • .addHelpText('after', CLOUD_RUN_HELP_EPILOG) on cloud_run, printing after
    commander's option block:

    Any option that is not listed above is forwarded verbatim to "gcloud run deploy".
    Use -- to separate gcloud arguments from adk arguments.
    
    Examples:
      adk deploy cloud_run --project=[project] --region=[region] path/to/my_agent
      adk deploy cloud_run path/to/my_agent -- --min-instances=2
    
    ADK sets --source, --project, --port, --verbosity and --region itself, and also
    reserves the gcloud env-var flags (--update-env-vars, --set-env-vars,
    --remove-env-vars, --clear-env-vars, --env-vars-file) when --a2a_auth_token is
    set. Passing a reserved flag as a gcloud argument is rejected.
    

    CLOUD_RUN_HELP_EPILOG is a module-level const next to the other CLI
    constants (not exported, not inlined into the chain), matching how this file
    already keeps its option constants at module scope.

Every factual claim in the epilog is derived from the code, not from memory:

Claim Source of truth
unrecognized options are forwarded to gcloud run deploy getExtraGcloudArgs (cli.ts) → extraGcloudArgs → appended to the argv in prepareGCloudArguments (cli_deploy_cloud_run.ts)
--source, --project, --port, --verbosity reserved adkManagedArgs in prepareGCloudArguments
--region reserved unconditionally adkManagedArgs.push('--region') is guarded by options.region, and deployToCloudRun resolves and assigns options.region (from --region or gcloud config get-value run/region, throwing if neither exists) before calling prepareGCloudArguments — so it is always set by then. Confirmed against the built CLI in all three paths, see Verifying the --region claim below
env-var flags reserved only with --a2a_auth_token the if (options.a2aAuthToken) branch in prepareGCloudArguments
-- separates gcloud args from adk args made true by the second commit below — getExtraGcloudArgs now drops every bare --, so the separator never reaches gcloud (pinned by three tests: the exact example the help prints, a mixed invocation, and a double separator)

Verifying the --region claim. It would be easy to read
if (options.region) adkManagedArgs.push('--region') as "reserved only when the
user passes --region". It is not: deployToCloudRun assigns options.region
before prepareGCloudArguments runs, so the flag can never be passed through.
Checked against the built CLI, all three paths:

# no --region flag, gcloud default region configured
$ adk deploy cloud_run ./a --project=example-project -- --region=us-west1
--region option is not provided, using default region from gcloud config: us-central1
[ADK CLI] Error deploying agent: The argument(s) --region conflict with ADK's automatic configuration. …

# no --region flag, no gcloud default region
$ adk deploy cloud_run ./a --project=example-project -- --region=us-west1
[ADK CLI] Error deploying agent: Region is not specified and default value for "run/region" is not set in gcloud config. …

# --region flag given
$ adk deploy cloud_run ./a --project=example-project --region=us-central1 -- --region=us-west1
[ADK CLI] Error deploying agent: The argument(s) --region conflict with ADK's automatic configuration. …

The epilog therefore states --region unconditionally, which is what the CLI
does. The one genuinely conditional item — the env-var flags — is written as a
condition.

Wording parity with the Python SDK. The epilog mirrors the
cli_deploy_cloud_run docstring in adk-python (Use '--' to separate gcloud arguments from adk arguments. plus the two worked examples), extended with the
reserved-flag list, which adk-python's docstring does not state but its
_validate_gcloud_extra_args enforces.

Second commit — make the documented -- separator actually work. Writing
"Use -- to separate gcloud arguments from adk arguments" into the help is only
honest if the separator survives the round trip, and it did not:

adk deploy cloud_run ./agent --no-allow-unauthenticated -- --min-instances=2
  => extraGcloudArgs ["--no-allow-unauthenticated", "--", "--min-instances=2"]
adk deploy cloud_run ./agent --memory 512Mi -- --min-instances=2
  => extraGcloudArgs ["--memory", "512Mi", "--", "--min-instances=2"]

The base of this stack (#447) reads the pass-through list off commander's parse
result, on the assumption that commander always consumes the terminator. It does
not: commander/lib/command.js handles the marker with
if (arg === '--') { if (dest === unknown) dest.push(arg); … }, and the first
unrecognized flag flips dest from operands to unknown. So the -- is
discarded only when it is the first unrecognized token; once any loose gcloud
flag precedes it, the marker is retained in command.args and copied straight
through. validateGcloudExtraArgs waves it past (a bare -- conflicts with
nothing), prepareGCloudArguments appends it to the argv, and — because
gcloud run deploy declares no trailing-remainder argument — the deploy aborts
with ERROR: (gcloud.run.deploy) unrecognized arguments: --, exit 2, after the
bundle and containerize work has already run.

Fix: getExtraGcloudArgs filters every bare -- out of what it returns. This is
the whole delta — one .filter and a comment explaining the commander behaviour
that makes it necessary. Dropping every bare marker rather than just the first
is deliberate: a second -- is equally unrecognizable to gcloud, so forwarding
it would only produce the same exit-2 failure. Python's click likewise never
passes the separator into extra_gcloud_args
(test_cli_deploy_cloud_run_allows_empty_gcloud_args).

Third commit — don't deploy a leading gcloud flag as the agent directory.
Commander binds the first unmatched token to the declared [agents_dir]
argument even when that token is an unknown flag being forwarded. So with the
directory omitted:

adk deploy cloud_run --allow-unauthenticated
adk deploy cloud_run -- --min-instances=2       # the separator form this help documents
  => agentPath <cwd>/--allow-unauthenticated   (deploy source does not exist)

The base of this stack fixed the forwarding half of that case (the flag now
reaches gcloud instead of being silently swallowed) but left the path half
broken, so the invocation still could not succeed. Resolving the argument
through resolveAgentPath — fall back to the argument's own process.cwd()
default when the value looks like a flag — completes it. An agent path never
starts with -; a relative one is spelled ./-name.

Not breaking: .description() and .addHelpText() cannot affect parsing; the
separator filter only changes invocations that use --, every one of which
either already worked (separator first — unchanged) or failed with the
unrecognized-argument error above; and resolveAgentPath only changes an agent
path that begins with -, which never resolved to an existing directory. There
is no working behaviour to preserve in either case. No new exported symbol, no
dependency, no lockfile change, no .md file touched.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.

Commands run (targeted only):

npx vitest run --project unit:dev dev/test/cli/cli_test.ts                    # 39 passed
npx vitest run --project unit:dev dev/test/cli/cli_deploy_cloud_run_test.ts   # 18 passed
npx tsc --noEmit -p dev/tsconfig.json                                          # clean
npm run lint / npm run format:check / npm run build                            # clean

Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.

Seven new cases in dev/test/cli/cli_test.ts, all added — no existing test was
edited, renamed, skipped or deleted:

  1. command: deploy > should list every deploy subcommand with a description
    asserts each of the three rows carries its description.
  2. command: deploy cloud_run > should document the gcloud pass-through contract in its help — asserts the description, the forwarding sentence, the --
    sentence, the worked -- example, the reserved-flag sentence, the env-var
    sentence, that the epilog comes after the option block, and that
    deployToCloudRun is never called by a help request.
  3. command: deploy cloud_run > should forward the -- example printed in its help — runs the literal example from the epilog
    (path/to/my_agent -- --min-instances=2) and asserts
    extraGcloudArgs === ['--min-instances=2'], tying the documentation to the
    behaviour.
  4. command: deploy cloud_run > should drop the -- separator when a gcloud flag precedes it — the mixed invocation that was broken:
    ./my-agent-path --no-allow-unauthenticated -- --min-instances=2 must forward
    ['--no-allow-unauthenticated', '--min-instances=2'].
  5. command: deploy cloud_run > should drop every bare -- from the forwarded gcloud args — a space-separated unknown flag plus two separators
    (--memory 512Mi -- --min-instances=2 -- --cpu=2) must forward
    ['--memory', '512Mi', '--min-instances=2', '--cpu=2'], pinning that no
    marker survives in any position.
  6. command: deploy cloud_run > should not deploy a leading gcloud flag as the agent directorydeploy cloud_run --allow-unauthenticated must resolve
    agentPath to the working directory, not to <cwd>/--allow-unauthenticated.
    Paired with the existing forwarding assertion for the same invocation rather
    than editing it.
  7. command: deploy cloud_run > should support the -- example with the agent directory omitteddeploy cloud_run -- --min-instances=2 must both resolve
    agentPath to the working directory and forward ['--min-instances=2'],
    i.e. the form this help documents works with or without the directory.

Tests 1-3 share one module-level helper, captureHelp(program, commandPath), which
drives the real --help path (program.parseAsync([... , '--help'])) rather
than calling a formatting method directly, and asserts the run exits with code
0 / commander.helpDisplayed. Two commander details it accounts for, both
verified against commander@14.0.3 (the version the lockfile resolves for
dev's "commander": "^14.0.0"):

  • helpInformation() does not include addHelpText output — the epilog is
    emitted by outputHelp() via the afterHelp event. Asserting on
    helpInformation() would silently miss it.
  • configureOutput() assigns a new _outputConfiguration object on the
    command it is called on, while subcommands captured the parent's object at
    creation time. Capturing on the root program therefore does not redirect a
    subcommand's help, so the helper configures (and exitOverrides) the command
    that renders the help.

Proof each new test can fail (mutations applied one at a time to the
already-passing tree, then reverted):

Mutation Result
Delete .description('Deploys an agent to Cloud Run') tests 1 and 2 fail: expected 'Usage: ADK CLI deploy [options] [comm…' to contain 'cloud_run [options] [agents_dir] Depl…' and expected 'Usage: ADK CLI deploy cloud_run [opti…' to contain 'Deploys an agent to Cloud Run'
Delete .addHelpText('after', CLOUD_RUN_HELP_EPILOG) test 2 fails: expected 'Usage: ADK CLI deploy cloud_run [opti…' to contain 'Any option that is not listed above i…' (1 failed, 34 passed)
Delete .description('Deploys an agent to Vertex AI Agent Engine') test 1 fails: expected 'Usage: ADK CLI deploy [options] [comm…' to contain 'agent_engine [options] [agents_dir] D…' (1 failed, 34 passed)
Drop the operand shift() in getExtraGcloudArgs (base behaviour this PR documents) test 3 fails: expected [ 'path/to/my_agent', …(1) ] to deeply equal [ '--min-instances=2' ]
Revert the fix — return extraArgs; instead of return extraArgs.filter(…) tests 4 and 5 fail: expected [ '--no-allow-unauthenticated', …(2) ] to deeply equal [ '--no-allow-unauthenticated', …(1) ] and expected [ '--memory', '512Mi', '--', …(3) ] to deeply equal [ '--memory', '512Mi', …(2) ] (2 failed, 35 passed)
Revert the fix — agentPath: getAbsolutePath(agentPath) instead of resolveAgentPath(agentPath) tests 6 and 7 fail: expected '/…/--allow-unauthenticated' to be '/…' // Object.is equality and expected { …(16) } to match object { …(2) } (2 failed, 37 passed)

Tests 4-7 were each written first and observed failing against the unfixed tree,
with exactly the output above, before the corresponding fix was applied.

Coverage. Every new source line is executed — statement hits for the new
lines, from --coverage.include='dev/src/cli/cli.ts':
[[118,17],[119,17],[130,17],[131,17],[231,1],[449,39],[478,39],[489,17],[514,78]].
Both new branches have both outcomes covered: the .filter predicate
([118, [21]]) and the resolveAgentPath ternary ([131, [3]] / [131, [14]]
— flag-shaped values and normal paths). Whole-file branch coverage moves up,
72.5% → 75%. The whole-file line number when running only this file is 95.41%;
every uncovered line (51-52, 95-96, 303-305, 348-350, 387-388, 438-439, 507-508,
562-563, 590-594) is pre-existing error-handling in other commands, none of them
in this diff.

Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.

Run against the real built CLI, no mocks (npm run build -w dev, then
node dev/dist/esm/cli_entrypoint.js …):

  1. adk deploy --help → exit 0, and the three rows now read:

    cloud_run [options] [agents_dir]         Deploys an agent to Cloud Run
    agent_engine [options] [agents_dir]      Deploys an agent to Vertex AI Agent Engine
    reasoning_engine [options] [agents_dir]  Deploys an agent to Vertex AI Agent Engine
    
  2. adk deploy cloud_run --help → exit 0, epilog printed immediately after the
    -h, --help row, exactly as quoted above.

  3. Reserved-flag claim, verified live (no network, no deploy — validation fails
    first), in both the plain and the previously-broken mixed form:

    $ adk deploy cloud_run ./fake_agent --project=example-project \
        --region=us-central1 -- --port=9999
    [ADK CLI] Error deploying agent: The argument(s) --port conflict with ADK's
    automatic configuration. ADK manages these arguments itself, so please remove
    them from your command.
    
    $ adk deploy cloud_run ./fake_agent --project=example-project \
        --region=us-central1 --no-allow-unauthenticated -- --port=9999
    [ADK CLI] Error deploying agent: The argument(s) --port conflict with ADK's
    automatic configuration. ADK manages these arguments itself, so please remove
    them from your command.
    

    Both forms carry the post-separator token through to
    validateGcloudExtraArgs, i.e. the epilog's claim holds whether or not a
    loose gcloud flag precedes the separator.

  4. Same check with the agent directory omitted — previously this could not get
    as far as validation, because --port=9999 was bound to [agents_dir]:

    $ adk deploy cloud_run --project=example-project --region=us-central1 \
        -- --port=9999
    [ADK CLI] Error deploying agent: The argument(s) --port conflict with ADK's
    automatic configuration. ADK manages these arguments itself, so please remove
    them from your command.
    
  5. Not run: an actual adk deploy cloud_run … -- --min-instances=2 against a
    live GCP project, which needs credentials and a billable deploy. The absence
    of the -- in the forwarded argv is not observable from outside the process
    validateGcloudExtraArgs ignores a bare marker and only gcloud itself
    rejects it — so it is asserted at the collector seam instead, by tests 3-5
    above.

Checklist

[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.


Collision check. gh pr list --repo AmaadMartin/adk-js --state open --limit 1000 (full list, not truncated), filtered for deploy/cloud-run/help-text work,
then gh pr diff --name-only on every plausibly adjacent PR:

Because this PR targets fix/cloud-run-passthrough-gcloud-args rather than
main, .github/workflows/validation.yaml (pull_request: branches: [main])
does not trigger, so no test job runs on it. Validation was done locally on the
pushed commit as recorded above.

Amaad Martin added 3 commits August 3, 2026 12:19
The three `adk deploy` subcommands were registered without a description,
so `adk deploy --help` printed three blank rows, and nothing in the repo
said that `adk deploy cloud_run` forwards unrecognized flags verbatim to
`gcloud run deploy` or that a handful of flags are reserved because ADK
sets them itself.

Add a description to each subcommand and an `addHelpText('after', ...)`
epilog on `cloud_run` that states the forwarding contract, the `--`
separator, worked examples, and the reserved flags. The epilog's reserved
list is derived from `prepareGCloudArguments` in
`dev/src/cli/deploy/cli_deploy_cloud_run.ts`.

No parsing behaviour changes.
`getExtraGcloudArgs` relied on commander to consume the end-of-options
marker, but commander only discards it while it is still collecting
operands: the first unrecognized flag switches the destination to the
unknown list, after which the `--` is kept in `command.args`. So a mixed
invocation such as

  adk deploy cloud_run ./agent --no-allow-unauthenticated -- --min-instances=2

forwarded `["--no-allow-unauthenticated", "--", "--min-instances=2"]`, and
`gcloud run deploy` (which declares no trailing-remainder argument) aborted
with `unrecognized arguments: --` after the bundle and containerize work had
already run.

Drop every bare `--` from the forwarded list. This matches the separator
contract the cloud_run help epilog documents, and Python's click, which
never passes the separator to `extra_gcloud_args` either.
Commander binds the first unmatched token to the declared `[agents_dir]`
argument even when that token is an unknown flag the command is forwarding,
so `adk deploy cloud_run --allow-unauthenticated` (and the documented
separator form with the directory omitted, `adk deploy cloud_run --
--min-instances=2`) resolved the deploy source to
`<cwd>/--allow-unauthenticated` and could never succeed. Forwarding the flag
without also fixing the path left the invocation broken.

Resolve the argument through `resolveAgentPath`, which falls back to the
argument's own `process.cwd()` default when the value looks like a flag; an
agent path never starts with `-`, since a relative one is spelled `./-name`.

Also tighten the reserved-flag paragraph of the cloud_run help epilog: state
the `--a2a_auth_token` condition as a condition, state the rejection once,
and drop the rationale that already lives as a comment next to the list it
describes in `cli_deploy_cloud_run.ts`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant